diff --git a/swh/web/ui/converters.py b/swh/web/ui/converters.py
--- a/swh/web/ui/converters.py
+++ b/swh/web/ui/converters.py
@@ -159,16 +159,31 @@
- sha1s are in hexadecimal strings (id, directory)
- bytes are decoded in string (author_name, committer_name,
author_email, committer_email, message)
+ - if message is not utf-8 encoded, message is null and msg_url points
+ to the api endpoint to download the revision message as is
- remaining keys are left as is
"""
- return from_swh(revision,
- hashess=set(['id', 'directory', 'parents', 'children']),
- bytess=set(['name',
- 'fullname',
- 'email',
- 'message']),
- dates={'date', 'committer_date'})
+ revision = from_swh(revision,
+ hashess=set(['id',
+ 'directory',
+ 'parents',
+ 'children']),
+ bytess=set(['name',
+ 'fullname',
+ 'email']),
+ dates={'date', 'committer_date'})
+
+ if revision:
+ if 'message' in revision:
+ try:
+ revision['message'] = revision['message'].decode('utf-8')
+ except UnicodeDecodeError:
+ revision['message'] = None
+ revision['msg_url'] = ('/api/1/revision/''%s/raw/'
+ % revision['id'])
+
+ return revision
def from_content(content):
diff --git a/swh/web/ui/service.py b/swh/web/ui/service.py
--- a/swh/web/ui/service.py
+++ b/swh/web/ui/service.py
@@ -216,7 +216,6 @@
'Only sha1_git is supported.')
res = backend.revision_get(sha1_git_bin)
- res.pop('message', None)
return converters.from_revision(res)
@@ -247,7 +246,7 @@
raise NotFoundExc('No message for revision with sha1_git %s.'
% rev_sha1_git)
res = {'message': revision['message']}
- return converters.from_revision(res)
+ return res
def lookup_revision_by(origin_id,
diff --git a/swh/web/ui/templates/revision-log.html b/swh/web/ui/templates/revision-log.html
--- a/swh/web/ui/templates/revision-log.html
+++ b/swh/web/ui/templates/revision-log.html
@@ -75,6 +75,14 @@
{% endif %}
+ {% if revision['msg_url'] is not none %}
+
+ {% endif %}
+
+
{% for key in revision.keys() %}
{% if key in ['type', 'synthetic'] and revision[key] is not none %}
diff --git a/swh/web/ui/templates/revision.html b/swh/web/ui/templates/revision.html
--- a/swh/web/ui/templates/revision.html
+++ b/swh/web/ui/templates/revision.html
@@ -57,6 +57,13 @@
{{ revision['message'] }}
{% endif %}
+
+ {% if revision['msg_url'] is not none %}
+
+ {% endif %}
{% for key in revision.keys() %}
{% if key in ['type', 'synthetic'] and revision[key] is not none %}
diff --git a/swh/web/ui/tests/test_converters.py b/swh/web/ui/tests/test_converters.py
--- a/swh/web/ui/tests/test_converters.py
+++ b/swh/web/ui/tests/test_converters.py
@@ -326,6 +326,108 @@
self.assertEqual(actual_revision, expected_revision)
@istest
+ def from_revision_invalid(self):
+ revision_input = {
+ 'id': hashutil.hex_to_hash(
+ '18d8be353ed3480476f032475e7c233eff7371d5'),
+ 'directory': hashutil.hex_to_hash(
+ '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6'),
+ 'author': {
+ 'name': b'Software Heritage',
+ 'fullname': b'robot robot@softwareheritage.org',
+ 'email': b'robot@softwareheritage.org',
+ },
+ 'committer': {
+ 'name': b'Software Heritage',
+ 'fullname': b'robot robot@softwareheritage.org',
+ 'email': b'robot@softwareheritage.org',
+ },
+ 'message': b'invalid message \xff',
+ 'date': {
+ 'timestamp': datetime.datetime(
+ 2000, 1, 17, 11, 23, 54,
+ tzinfo=datetime.timezone.utc).timestamp(),
+ 'offset': 0,
+ 'negative_utc': False,
+ },
+ 'committer_date': {
+ 'timestamp': datetime.datetime(
+ 2000, 1, 17, 11, 23, 54,
+ tzinfo=datetime.timezone.utc).timestamp(),
+ 'offset': 0,
+ 'negative_utc': False,
+ },
+ 'synthetic': True,
+ 'type': 'tar',
+ 'parents': [
+ hashutil.hex_to_hash(
+ '29d8be353ed3480476f032475e7c244eff7371d5'),
+ hashutil.hex_to_hash(
+ '30d8be353ed3480476f032475e7c244eff7371d5')
+ ],
+ 'children': [
+ hashutil.hex_to_hash(
+ '123546353ed3480476f032475e7c244eff7371d5'),
+ ],
+ 'metadata': {
+ 'original_artifact': [{
+ 'archive_type': 'tar',
+ 'name': 'webbase-5.7.0.tar.gz',
+ 'sha1': '147f73f369733d088b7a6fa9c4e0273dcd3c7ccd',
+ 'sha1_git': '6a15ea8b881069adedf11feceec35588f2cfe8f1',
+ 'sha256': '401d0df797110bea805d358b85bcc1ced29549d3d73f'
+ '309d36484e7edf7bb912',
+
+ }]
+ },
+ }
+
+ expected_revision = {
+ 'id': '18d8be353ed3480476f032475e7c233eff7371d5',
+ 'directory': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6',
+ 'author': {
+ 'name': 'Software Heritage',
+ 'fullname': 'robot robot@softwareheritage.org',
+ 'email': 'robot@softwareheritage.org',
+ },
+ 'committer': {
+ 'name': 'Software Heritage',
+ 'fullname': 'robot robot@softwareheritage.org',
+ 'email': 'robot@softwareheritage.org',
+ },
+ 'message': None,
+ 'msg_url': '/api/1/revision/'
+ '18d8be353ed3480476f032475e7c233eff7371d5/raw/',
+ 'date': "2000-01-17T11:23:54+00:00",
+ 'committer_date': "2000-01-17T11:23:54+00:00",
+ 'children': [
+ '123546353ed3480476f032475e7c244eff7371d5'
+ ],
+ 'parents': [
+ '29d8be353ed3480476f032475e7c244eff7371d5',
+ '30d8be353ed3480476f032475e7c244eff7371d5'
+ ],
+ 'type': 'tar',
+ 'synthetic': True,
+ 'metadata': {
+ 'original_artifact': [{
+ 'archive_type': 'tar',
+ 'name': 'webbase-5.7.0.tar.gz',
+ 'sha1': '147f73f369733d088b7a6fa9c4e0273dcd3c7ccd',
+ 'sha1_git': '6a15ea8b881069adedf11feceec35588f2cfe8f1',
+ 'sha256': '401d0df797110bea805d358b85bcc1ced29549d3d73f'
+ '309d36484e7edf7bb912'
+ }]
+ },
+ }
+
+ # when
+ actual_revision = converters.from_revision(revision_input)
+
+ # then
+ self.assertEqual(actual_revision, expected_revision)
+
+ @istest
def from_content(self):
content_input = {
'sha1': hashutil.hex_to_hash('5c6f0e2750f48fa0bd0c4cf5976ba0b9e0'
diff --git a/swh/web/ui/tests/test_service.py b/swh/web/ui/tests/test_service.py
--- a/swh/web/ui/tests/test_service.py
+++ b/swh/web/ui/tests/test_service.py
@@ -929,6 +929,76 @@
'name': 'boule & bill',
'email': 'boule@bill.org',
},
+ 'message': 'elegant fix for bug 31415957',
+ 'date': "2000-01-17T11:23:54+00:00",
+ 'committer_date': "2000-01-17T11:23:54+00:00",
+ 'synthetic': False,
+ 'type': 'git',
+ 'parents': [],
+ 'metadata': [],
+ })
+
+ mock_backend.revision_get.assert_called_with(
+ hex_to_hash('18d8be353ed3480476f032475e7c233eff7371d5'))
+
+ @patch('swh.web.ui.service.backend')
+ @istest
+ def lookup_revision_invalid_msg(self, mock_backend):
+ # given
+ stub_rev = {
+ 'id': hex_to_hash('123456'),
+ 'directory': hex_to_hash(
+ '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6'),
+ 'author': {
+ 'name': b'bill & boule',
+ 'email': b'bill@boule.org',
+ },
+ 'committer': {
+ 'name': b'boule & bill',
+ 'email': b'boule@bill.org',
+ },
+ 'message': b'elegant fix for bug \xff',
+ 'date': {
+ 'timestamp': datetime.datetime(
+ 2000, 1, 17, 11, 23, 54,
+ tzinfo=datetime.timezone.utc,
+ ).timestamp(),
+ 'offset': 0,
+ 'negative_utc': False,
+ },
+ 'committer_date': {
+ 'timestamp': datetime.datetime(
+ 2000, 1, 17, 11, 23, 54,
+ tzinfo=datetime.timezone.utc,
+ ).timestamp(),
+ 'offset': 0,
+ 'negative_utc': False,
+ },
+ 'synthetic': False,
+ 'type': 'git',
+ 'parents': [],
+ 'metadata': [],
+ }
+ mock_backend.revision_get = MagicMock(return_value=stub_rev)
+
+ # when
+ actual_revision = service.lookup_revision(
+ '18d8be353ed3480476f032475e7c233eff7371d5')
+
+ # then
+ self.assertEqual(actual_revision, {
+ 'id': '123456',
+ 'directory': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6',
+ 'author': {
+ 'name': 'bill & boule',
+ 'email': 'bill@boule.org',
+ },
+ 'committer': {
+ 'name': 'boule & bill',
+ 'email': 'boule@bill.org',
+ },
+ 'message': None,
+ 'msg_url': '/api/1/revision/123456/raw/',
'date': "2000-01-17T11:23:54+00:00",
'committer_date': "2000-01-17T11:23:54+00:00",
'synthetic': False,
@@ -984,7 +1054,7 @@
'18d8be353ed3480476f032475e7c233eff7371d5')
# then
- self.assertEquals(rv, {'message': 'elegant fix for bug 31415957'})
+ self.assertEquals(rv, {'message': b'elegant fix for bug 31415957'})
mock_backend.revision_get.assert_called_with(
hex_to_hash('18d8be353ed3480476f032475e7c233eff7371d5'))
diff --git a/swh/web/ui/tests/views/test_api.py b/swh/web/ui/tests/views/test_api.py
--- a/swh/web/ui/tests/views/test_api.py
+++ b/swh/web/ui/tests/views/test_api.py
@@ -723,16 +723,12 @@
mock_service.lookup_revision_message.return_value = stub_revision
# when
- rv = self.app.get('/api/1/revision/'
- '18d8be353ed3480476f032475e7c233eff7371d5/raw/')
-
+ rv = self.app.get('/api/1/revision/18d8be353ed3480476f032475e7c2'
+ '33eff7371d5/raw/')
# then
self.assertEquals(rv.status_code, 200)
- self.assertEquals(rv.mimetype, 'application/json')
-
- response_data = json.loads(rv.data.decode('utf-8'))
- self.assertEquals(response_data,
- {'message': 'synthetic revision message'})
+ self.assertEquals(rv.mimetype, 'application/octet-stream')
+ self.assertEquals(rv.data, b'synthetic revision message')
mock_service.lookup_revision_message.assert_called_once_with(
'18d8be353ed3480476f032475e7c233eff7371d5')
diff --git a/swh/web/ui/tests/views/test_browse.py b/swh/web/ui/tests/views/test_browse.py
--- a/swh/web/ui/tests/views/test_browse.py
+++ b/swh/web/ui/tests/views/test_browse.py
@@ -839,6 +839,19 @@
@patch('swh.web.ui.views.browse.api')
@istest
+ def browse_revision_raw_message(self, mock_api):
+ # given
+ sha1 = 'd770e558e21961ad6cfdf0ff7df0eb5d7d4f0754'
+
+ # when
+ rv = self.client.get('/browse/revision/'
+ 'd770e558e21961ad6cfdf0ff7df0eb5d7d4f0754/raw/')
+
+ self.assertRedirects(
+ rv, '/api/1/revision/%s/raw/' % sha1)
+
+ @patch('swh.web.ui.views.browse.api')
+ @istest
def browse_revision_log_ko_not_found(self, mock_api):
# given
mock_api.api_revision_log.side_effect = NotFoundExc('Not found!')
diff --git a/swh/web/ui/views/api.py b/swh/web/ui/views/api.py
--- a/swh/web/ui/views/api.py
+++ b/swh/web/ui/views/api.py
@@ -512,8 +512,11 @@
GET /api/1/revision/baf18f9fc50a0b6fef50460a76c33b2ddc57486e/raw/
"""
-
- return service.lookup_revision_message(sha1_git)
+ raw = service.lookup_revision_message(sha1_git)
+ return Response(raw['message'],
+ headers={'Content-disposition': 'attachment;'
+ 'filename=rev_%s_raw' % sha1_git},
+ mimetype='application/octet-stream')
@app.route('/api/1/revision//directory/')
diff --git a/swh/web/ui/views/browse.py b/swh/web/ui/views/browse.py
--- a/swh/web/ui/views/browse.py
+++ b/swh/web/ui/views/browse.py
@@ -332,6 +332,14 @@
return render_template('revision.html', **env)
+@app.route('/browse/revision//raw/')
+def browse_revision_raw_message(sha1_git):
+ """Given a sha1_git, display the corresponding revision's raw message.
+
+ """
+ return redirect(url_for('api_revision_raw_message', sha1_git=sha1_git))
+
+
@app.route('/browse/revision//log/')
@set_renderers(HTMLRenderer)
def browse_revision_log(sha1_git):